/*
 * $Id: SearchFactory.java 3927 2011-02-22 16:34:11Z kleopatra $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.swingx.search;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.Frame;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

import org.jdesktop.swingx.JXDialog;
import org.jdesktop.swingx.JXFindBar;
import org.jdesktop.swingx.JXFindPanel;
import org.jdesktop.swingx.JXFrame;
import org.jdesktop.swingx.JXRootPane;
import org.jdesktop.swingx.plaf.LookAndFeelAddons;
import org.jdesktop.swingx.plaf.UIDependent;
import org.jdesktop.swingx.util.Utilities;

/**
 * Factory to create, configure and show application consistent search and find
 * widgets.
 * 
 * Typically a shared JXFindBar is used for incremental search, while a shared
 * JXFindPanel is used for batch search. This implementation
 * 
 * <ul>
 * <li>JXFindBar - adds and shows it in the target's toplevel container's
 * toolbar (assuming a JXRootPane)
 * <li>JXFindPanel - creates a JXDialog, adds and shows the findPanel in the
 * Dialog
 * </ul>
 * 
 * 
 * PENDING: JW - update (?) views/wiring on focus change. Started brute force -
 * stop searching. This looks extreme confusing for findBars added to ToolBars
 * which are empty except for the findbar. Weird problem if triggered from menu
 * - find widget disappears after having been shown for an instance. Where's the
 * focus?
 * 
 * 
 * PENDING: add methods to return JXSearchPanels (for use by PatternMatchers).
 * 
 * @author Jeanette Winzenburg
 */
public class SearchFactory implements UIDependent {
	private static class LaFListener implements PropertyChangeListener {
		private final WeakReference<SearchFactory> ref;

		public LaFListener(SearchFactory sf) {
			this.ref = new WeakReference<SearchFactory>(sf);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void propertyChange(PropertyChangeEvent evt) {
			SearchFactory sf = ref.get();

			if (sf == null) {
				UIManager.removePropertyChangeListener(this);
			} else if ("lookAndFeel".equals(evt.getPropertyName())) {
				sf.updateUI();
			}
		}
	}

	// PENDING: rename methods to batch/incremental instead of dialog/toolbar

	static {
		// Hack to enforce loading of SwingX framework ResourceBundle
		LookAndFeelAddons.getAddon();
	}

	private static SearchFactory searchFactory;

	/** the shared find widget for batch-find. */
	protected JXFindPanel findPanel;

	/** the shared find widget for incremental-find. */
	protected JXFindBar findBar;
	/**
	 * this is a temporary hack: need to remove the useSearchHighlighter
	 * property.
	 */
	protected JComponent lastFindBarTarget;

	private boolean useFindBar;

	private Point lastFindDialogLocation;

	private FindRemover findRemover;

	/**
	 * Returns the shared SearchFactory.
	 * 
	 * @return the shared <code>SearchFactory</code>
	 */
	public static SearchFactory getInstance() {
		if (searchFactory == null) {
			searchFactory = new SearchFactory();
		}
		return searchFactory;
	}

	/**
	 * Sets the shared SearchFactory.
	 * 
	 * @param factory
	 */
	public static void setInstance(SearchFactory factory) {
		searchFactory = factory;
	}

	public SearchFactory() {
		UIManager.addPropertyChangeListener(new LaFListener(this));
	}

	/**
	 * Returns a common Keystroke for triggering a search. Tries to be
	 * OS-specific.
	 * <p>
	 * 
	 * PENDING: this should be done in the LF and the keyStroke looked up in the
	 * UIManager.
	 * 
	 * @return the keyStroke to register with a findAction.
	 */
	public KeyStroke getSearchAccelerator() {
		// JW: this should be handled by the LF!
		// get the accelerator mnemonic from the UIManager
		String findMnemonic = "F";
		KeyStroke findStroke = Utilities.stringToKey("D-" + findMnemonic);
		// fallback for sandbox (this should be handled in Utilities instead!)
		if (findStroke == null) {
			findStroke = KeyStroke.getKeyStroke("control F");
		}
		return findStroke;

	}

	/**
	 * Returns decision about using a batch- vs. incremental-find for the
	 * searchable. This implementation returns the useFindBar property directly.
	 * 
	 * @param target
	 *            - the component associated with the searchable
	 * @param searchable
	 *            - the object to search.
	 * @return true if a incremental-find should be used, false otherwise.
	 */
	public boolean isUseFindBar(JComponent target, Searchable searchable) {
		return useFindBar;
	}

	/**
	 * Sets the default search type to incremental or batch, for a true/false
	 * boolean. The default value is false (== batch).
	 * 
	 * @param incremental
	 *            a boolean to indicate the default search type, true for
	 *            incremental and false for batch.
	 */
	public void setUseFindBar(boolean incremental) {
		if (incremental == useFindBar)
			return;
		this.useFindBar = incremental;
		getFindRemover().endSearching();
	}

	/**
	 * Shows an appropriate find widget targeted at the searchable. Opens a
	 * batch-find or incremental-find widget based on the return value of
	 * <code>isUseFindBar</code>.
	 * 
	 * @param target
	 *            - the component associated with the searchable
	 * @param searchable
	 *            - the object to search.
	 * 
	 * @see #isUseFindBar(JComponent, Searchable)
	 * @see #setUseFindBar(boolean)
	 */
	public void showFindInput(JComponent target, Searchable searchable) {
		if (isUseFindBar(target, searchable)) {
			showFindBar(target, searchable);
		} else {
			showFindDialog(target, searchable);
		}
	}

	// ------------------------- incremental search

	/**
	 * Show a incremental-find widget targeted at the searchable.
	 * 
	 * This implementation uses a JXFindBar and inserts it into the target's
	 * toplevel container toolbar.
	 * 
	 * PENDING: Nothing shown if there is no toolbar found.
	 * 
	 * @param target
	 *            - the component associated with the searchable
	 * @param searchable
	 *            - the object to search.
	 */
	public void showFindBar(JComponent target, Searchable searchable) {
		if (target == null)
			return;
		if (findBar == null) {
			findBar = getSharedFindBar();
		} else {
			releaseFindBar();
		}
		Window topLevel = SwingUtilities.getWindowAncestor(target);
		if (topLevel instanceof JXFrame) {
			JXRootPane rootPane = ((JXFrame) topLevel).getRootPaneExt();
			JToolBar toolBar = rootPane.getToolBar();
			if (toolBar == null) {
				toolBar = new JToolBar();
				rootPane.setToolBar(toolBar);
			}
			toolBar.add(findBar, 0);
			rootPane.revalidate();
			KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent(findBar);

		}
		lastFindBarTarget = target;
		findBar.setLocale(target.getLocale());
		target.putClientProperty(AbstractSearchable.MATCH_HIGHLIGHTER, Boolean.TRUE);
		getSharedFindBar().setSearchable(searchable);
		installFindRemover(target, findBar);
	}

	/**
	 * Returns the shared JXFindBar. Creates and configures on first call.
	 * 
	 * @return the shared <code>JXFindBar</code>
	 */
	public JXFindBar getSharedFindBar() {
		if (findBar == null) {
			findBar = createFindBar();
			configureSharedFindBar();
		}
		return findBar;
	}

	/**
	 * Factory method to create a JXFindBar.
	 * 
	 * @return the <code>JXFindBar</code>
	 */
	public JXFindBar createFindBar() {
		return new JXFindBar();
	}

	protected void installFindRemover(Container target, Container findWidget) {
		if (target != null) {
			getFindRemover().addTarget(target);
		}
		getFindRemover().addTarget(findWidget);
	}

	private FindRemover getFindRemover() {
		if (findRemover == null) {
			findRemover = new FindRemover();
		}
		return findRemover;
	}

	/**
	 * convenience method to remove a component from its parent and revalidate
	 * the parent
	 */
	protected void removeFromParent(JComponent component) {
		Container oldParent = component.getParent();
		if (oldParent != null) {
			oldParent.remove(component);
			if (oldParent instanceof JComponent) {
				((JComponent) oldParent).revalidate();
			} else {
				// not sure... never have non-j comps
				oldParent.invalidate();
				oldParent.validate();
			}
		}
	}

	protected void stopSearching() {
		if (findPanel != null) {
			lastFindDialogLocation = hideSharedFindPanel(false);
			findPanel.setSearchable(null);
		}
		if (findBar != null) {
			releaseFindBar();
		}
	}

	/**
	 * Pre: findbar != null.
	 */
	protected void releaseFindBar() {
		findBar.setSearchable(null);
		if (lastFindBarTarget != null) {
			lastFindBarTarget.putClientProperty(AbstractSearchable.MATCH_HIGHLIGHTER, Boolean.FALSE);
			lastFindBarTarget = null;
		}
		removeFromParent(findBar);
	}

	/**
	 * Configures the shared FindBar. This method is called once after creation
	 * of the shared FindBar. Subclasses can override to add configuration code.
	 * <p>
	 * 
	 * Here: registers a custom action to remove the findbar from its ancestor
	 * container.
	 * 
	 * PRE: findBar != null.
	 *
	 */
	protected void configureSharedFindBar() {
		Action removeAction = new AbstractAction() {

			@Override
			public void actionPerformed(ActionEvent e) {
				removeFromParent(findBar);
				// stopSearching();
				// releaseFindBar();

			}

		};
		findBar.getActionMap().put(JXDialog.CLOSE_ACTION_COMMAND, removeAction);
	}

	// ------------------------ batch search

	/**
	 * Show a batch-find widget targeted at the given Searchable.
	 * 
	 * This implementation uses a shared JXFindPanel contained JXDialog.
	 * 
	 * @param target
	 *            - the component associated with the searchable
	 * @param searchable
	 *            - the object to search.
	 */
	public void showFindDialog(JComponent target, Searchable searchable) {
		Window frame = null; // JOptionPane.getRootFrame();
		if (target != null) {
			target.putClientProperty(AbstractSearchable.MATCH_HIGHLIGHTER, Boolean.FALSE);
			frame = SwingUtilities.getWindowAncestor(target);
			// if (window instanceof Frame) {
			// frame = (Frame) window;
			// }
		}
		JXDialog topLevel = getDialogForSharedFindPanel();
		JXDialog findDialog;
		if ((topLevel != null) && (topLevel.getOwner().equals(frame))) {
			findDialog = topLevel;
			// JW: #635-swingx - quick hack to update title to current locale
			// ...
			// findDialog.setTitle(getSharedFindPanel().getName());
			KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent(findDialog);
		} else {
			Point location = hideSharedFindPanel(true);
			if (frame instanceof Frame) {
				findDialog = new JXDialog((Frame) frame, getSharedFindPanel());
			} else if (frame instanceof Dialog) {
				// fix #215-swingx: had problems with secondary modal dialogs.
				findDialog = new JXDialog((Dialog) frame, getSharedFindPanel());
			} else {
				findDialog = new JXDialog(JOptionPane.getRootFrame(), getSharedFindPanel());
			}
			// RJO: shouldn't we avoid overloaded useage like this in a JSR296
			// world? swap getName() for getTitle() here?
			// findDialog.setTitle(getSharedFindPanel().getName());
			// JW: don't - this will stay on top of all applications!
			// findDialog.setAlwaysOnTop(true);
			findDialog.pack();
			if (location == null) {
				findDialog.setLocationRelativeTo(frame);
			} else {
				findDialog.setLocation(location);
			}
		}
		if (target != null) {
			findDialog.setLocale(target.getLocale());
		}
		getSharedFindPanel().setSearchable(searchable);
		installFindRemover(target, findDialog);
		findDialog.setVisible(true);
	}

	/**
	 * Returns the shared JXFindPanel. Lazyly creates and configures on first
	 * call.
	 * 
	 * @return the shared <code>JXFindPanel</code>
	 */
	public JXFindPanel getSharedFindPanel() {
		if (findPanel == null) {
			findPanel = createFindPanel();
			configureSharedFindPanel();
		} else {
			// JW: temporary hack around #718-swingx
			// no longer needed with cleanup of hideSharedFindPanel
			// if (findPanel.getParent() == null) {
			// SwingUtilities.updateComponentTreeUI(findPanel);
			// }
		}
		return findPanel;
	}

	/**
	 * Factory method to create a JXFindPanel.
	 * 
	 * @return <code>JXFindPanel</code>
	 */
	public JXFindPanel createFindPanel() {
		return new JXFindPanel();
	}

	/**
	 * Configures the shared FindPanel. This method is called once after
	 * creation of the shared FindPanel. Subclasses can override to add
	 * configuration code.
	 * <p>
	 * 
	 * Here: no-op PRE: findPanel != null.
	 *
	 */
	protected void configureSharedFindPanel() {
	}

	private JXDialog getDialogForSharedFindPanel() {
		if (findPanel == null)
			return null;
		Window window = SwingUtilities.getWindowAncestor(findPanel);
		return (window instanceof JXDialog) ? (JXDialog) window : null;
	}

	/**
	 * Hides the findPanel's toplevel window and returns its location. If the
	 * dispose is true, the findPanel is removed from its parent and the
	 * toplevel window is disposed.
	 * 
	 * @param dispose
	 *            boolean to indicate whether the findPanels toplevel window
	 *            should be disposed.
	 * @return the location of the window if visible, or the last known
	 *         location.
	 */
	protected Point hideSharedFindPanel(boolean dispose) {
		if (findPanel == null)
			return null;
		Window window = SwingUtilities.getWindowAncestor(findPanel);
		Point location = lastFindDialogLocation;
		if (window != null) {
			// PENDING JW: can't remember why it it removed always?
			if (window.isVisible()) {
				location = window.getLocationOnScreen();
				window.setVisible(false);
			}
			if (dispose) {
				findPanel.getParent().remove(findPanel);
				window.dispose();
			}
		}
		return location;
	}

	public class FindRemover implements PropertyChangeListener {
		KeyboardFocusManager focusManager;
		Set<Container> targets;

		public FindRemover() {
			updateManager();
		}

		public void addTarget(Container target) {
			getTargets().add(target);
		}

		public void removeTarget(Container target) {
			getTargets().remove(target);
		}

		private Set<Container> getTargets() {
			if (targets == null) {
				targets = new HashSet<Container>();
			}
			return targets;
		}

		private void updateManager() {
			if (focusManager != null) {
				focusManager.removePropertyChangeListener("permanentFocusOwner", this);
			}
			this.focusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
			focusManager.addPropertyChangeListener("permanentFocusOwner", this);
		}

		@Override
		public void propertyChange(PropertyChangeEvent ev) {

			Component c = focusManager.getPermanentFocusOwner();
			if (c == null)
				return;
			for (Iterator<Container> iter = getTargets().iterator(); iter.hasNext();) {
				Container element = iter.next();
				if ((element == c) || (SwingUtilities.isDescendingFrom(c, element))) {
					return;
				}
			}
			endSearching();
		}

		public void endSearching() {
			getTargets().clear();
			stopSearching();
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void updateUI() {
		if (findBar != null) {
			SwingUtilities.updateComponentTreeUI(findBar);
		}

		if (findPanel != null) {
			SwingUtilities.updateComponentTreeUI(findPanel);
		}
	}
}
